﻿// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.SecretsManager.Controllers;

[Authorize("secrets")]
[Route("service-accounts")]
public class ServiceAccountsController : Controller
{
    private readonly ICurrentContext _currentContext;
    private readonly IUserService _userService;
    private readonly IAuthorizationService _authorizationService;
    private readonly IServiceAccountRepository _serviceAccountRepository;
    private readonly IApiKeyRepository _apiKeyRepository;
    private readonly IOrganizationRepository _organizationRepository;
    private readonly ICountNewServiceAccountSlotsRequiredQuery _countNewServiceAccountSlotsRequiredQuery;
    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
    private readonly IServiceAccountSecretsDetailsQuery _serviceAccountSecretsDetailsQuery;
    private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
    private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
    private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
    private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
    private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
    private readonly IPricingClient _pricingClient;

    public ServiceAccountsController(
        ICurrentContext currentContext,
        IUserService userService,
        IAuthorizationService authorizationService,
        IServiceAccountRepository serviceAccountRepository,
        IApiKeyRepository apiKeyRepository,
        IOrganizationRepository organizationRepository,
        ICountNewServiceAccountSlotsRequiredQuery countNewServiceAccountSlotsRequiredQuery,
        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
        IServiceAccountSecretsDetailsQuery serviceAccountSecretsDetailsQuery,
        ICreateAccessTokenCommand createAccessTokenCommand,
        ICreateServiceAccountCommand createServiceAccountCommand,
        IUpdateServiceAccountCommand updateServiceAccountCommand,
        IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
        IRevokeAccessTokensCommand revokeAccessTokensCommand,
        IPricingClient pricingClient)
    {
        _currentContext = currentContext;
        _userService = userService;
        _authorizationService = authorizationService;
        _serviceAccountRepository = serviceAccountRepository;
        _apiKeyRepository = apiKeyRepository;
        _organizationRepository = organizationRepository;
        _countNewServiceAccountSlotsRequiredQuery = countNewServiceAccountSlotsRequiredQuery;
        _serviceAccountSecretsDetailsQuery = serviceAccountSecretsDetailsQuery;
        _createServiceAccountCommand = createServiceAccountCommand;
        _updateServiceAccountCommand = updateServiceAccountCommand;
        _deleteServiceAccountsCommand = deleteServiceAccountsCommand;
        _revokeAccessTokensCommand = revokeAccessTokensCommand;
        _pricingClient = pricingClient;
        _createAccessTokenCommand = createAccessTokenCommand;
        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
    }

    [HttpGet("/organizations/{organizationId}/service-accounts")]
    public async Task<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>> ListByOrganizationAsync(
        [FromRoute] Guid organizationId, [FromQuery] bool includeAccessToSecrets = false)
    {
        if (!_currentContext.AccessSecretsManager(organizationId))
        {
            throw new NotFoundException();
        }

        var userId = _userService.GetProperUserId(User).Value;
        var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);

        var results =
            await _serviceAccountSecretsDetailsQuery.GetManyByOrganizationIdAsync(organizationId, userId, accessClient,
                includeAccessToSecrets);
        var responses = results.Select(r => new ServiceAccountSecretsDetailsResponseModel(r));
        return new ListResponseModel<ServiceAccountSecretsDetailsResponseModel>(responses);
    }

    [HttpGet("{id}")]
    public async Task<ServiceAccountResponseModel> GetByServiceAccountIdAsync(
        [FromRoute] Guid id)
    {
        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
        var authorizationResult =
            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Read);

        if (!authorizationResult.Succeeded)
        {
            throw new NotFoundException();
        }

        return new ServiceAccountResponseModel(serviceAccount);
    }

    [HttpPost("/organizations/{organizationId}/service-accounts")]
    public async Task<ServiceAccountResponseModel> CreateAsync([FromRoute] Guid organizationId,
        [FromBody] ServiceAccountCreateRequestModel createRequest)
    {
        var serviceAccount = createRequest.ToServiceAccount(organizationId);
        var authorizationResult =
            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Create);

        if (!authorizationResult.Succeeded)
        {
            throw new NotFoundException();
        }

        var newServiceAccountSlotsRequired = await _countNewServiceAccountSlotsRequiredQuery
            .CountNewServiceAccountSlotsRequiredAsync(organizationId, 1);
        if (newServiceAccountSlotsRequired > 0)
        {
            var org = await _organizationRepository.GetByIdAsync(organizationId);
            // TODO: https://bitwarden.atlassian.net/browse/PM-17002
            var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType);
            var update = new SecretsManagerSubscriptionUpdate(org, plan, true)
                .AdjustServiceAccounts(newServiceAccountSlotsRequired);
            await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
        }

        var userId = _userService.GetProperUserId(User).Value;
        var result =
            await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId);
        return new ServiceAccountResponseModel(result);
    }

    [HttpPut("{id}")]
    public async Task<ServiceAccountResponseModel> UpdateAsync([FromRoute] Guid id,
        [FromBody] ServiceAccountUpdateRequestModel updateRequest)
    {
        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
        var authorizationResult =
            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Update);

        if (!authorizationResult.Succeeded)
        {
            throw new NotFoundException();
        }

        var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id));
        return new ServiceAccountResponseModel(result);
    }

    [HttpPost("delete")]
    public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
    {
        var serviceAccounts = (await _serviceAccountRepository.GetManyByIds(ids)).ToList();
        if (!serviceAccounts.Any() || serviceAccounts.Count != ids.Count)
        {
            throw new NotFoundException();
        }

        // Ensure all service accounts belong to the same organization
        var organizationId = serviceAccounts.First().OrganizationId;
        if (serviceAccounts.Any(sa => sa.OrganizationId != organizationId) ||
            !_currentContext.AccessSecretsManager(organizationId))
        {
            throw new NotFoundException();
        }

        var serviceAccountsToDelete = new List<ServiceAccount>();
        var results = new List<(ServiceAccount ServiceAccount, string Error)>();

        foreach (var serviceAccount in serviceAccounts)
        {
            var authorizationResult =
                await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Delete);
            if (authorizationResult.Succeeded)
            {
                serviceAccountsToDelete.Add(serviceAccount);
                results.Add((serviceAccount, ""));
            }
            else
            {
                results.Add((serviceAccount, "access denied"));
            }
        }

        await _deleteServiceAccountsCommand.DeleteServiceAccounts(serviceAccountsToDelete);
        var responses = results.Select(r => new BulkDeleteResponseModel(r.ServiceAccount.Id, r.Error));
        return new ListResponseModel<BulkDeleteResponseModel>(responses);
    }

    [HttpGet("{id}/access-tokens")]
    public async Task<ListResponseModel<AccessTokenResponseModel>> GetAccessTokens([FromRoute] Guid id)
    {
        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
        var authorizationResult =
            await _authorizationService.AuthorizeAsync(User, serviceAccount,
                ServiceAccountOperations.ReadAccessTokens);

        if (!authorizationResult.Succeeded)
        {
            throw new NotFoundException();
        }

        var accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(id);
        var responses = accessTokens.Select(token => new AccessTokenResponseModel(token));
        return new ListResponseModel<AccessTokenResponseModel>(responses);
    }

    [HttpPost("{id}/access-tokens")]
    public async Task<AccessTokenCreationResponseModel> CreateAccessTokenAsync([FromRoute] Guid id,
        [FromBody] AccessTokenCreateRequestModel request)
    {
        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
        var authorizationResult =
            await _authorizationService.AuthorizeAsync(User, serviceAccount,
                ServiceAccountOperations.CreateAccessToken);

        if (!authorizationResult.Succeeded)
        {
            throw new NotFoundException();
        }

        var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id));
        return new AccessTokenCreationResponseModel(result);
    }

    [HttpPost("{id}/access-tokens/revoke")]
    public async Task RevokeAccessTokensAsync(Guid id, [FromBody] RevokeAccessTokensRequest request)
    {
        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
        var authorizationResult =
            await _authorizationService.AuthorizeAsync(User, serviceAccount,
                ServiceAccountOperations.RevokeAccessTokens);

        if (!authorizationResult.Succeeded)
        {
            throw new NotFoundException();
        }

        await _revokeAccessTokensCommand.RevokeAsync(serviceAccount, request.Ids);
    }
}
